Core Java & Languages

Java Module und JPMS: Endlich bereit für den Einsatz?

Vorteile, Herausforderungen & Best Practices

Jörg Hohwiller

Wer im Web nach Java-Modulen oder dem Java Platform Module System (JPMS) sucht, stößt vor allem auf Kritik. Ist das JPMS also eher Flop als top? Auch wenn Module nur sehr langsam im Java Ecosystem angenommen werden, gibt es Projekte, die sie mit großem Erfolg einsetzen. Wir schauen die Vor- und Nachteile an, erfahren, mit welchen Best Practices man ein großes Projekt mit JPMS zum Erfolg führen kann, betrachten, welche Probleme es gibt, wann der Einsatz von JPMS sinnvoll ist, und zeigen, was Entwickler von (Open-Source-)Bibliotheken auch dann beachten sollten, wenn sie kein Interesse an Java-Modulen haben.

Jeder Java-Entwickler kennt das: Die Sichtbarkeiten public, protected, default und private sind nicht immer hilfreich. Wer hat sich nicht schon gewünscht, dass etwas nur für das eigene Package sowie für Sub-Packages sichtbar wäre? Noch schlimmer ist das Problem der Namespace Pollution: In einem großen Projekt sammeln sich unzählige Bibliotheken im Classpath, und die Auto Completion für generische Begriffe wie List, Entity, Metadata oder StringUtil liefert alle möglichen und unmöglichen Treffer und im Worst Case den richtigen erst ganz am Ende der Liste (Abb. 1).

Abb. 1: Namespace Pollution

Lösungen für diese Probleme sowie eine Alternative für den mit Java 17 abgekündigten Security Manager [1] bieten Java-Module. Der Einsatz von Modulen ist eine grundlegende Entscheidung, da das Java-Modul-System nur ganz oder gar nicht verwendet werden kann. Wenn man es nutzt, kommen neben den Vorteilen auch grundsätzliche Veränderungen im Verhalten dazu. Im Open-Source-Projekt m-m-m [2] nutze ich das JPMS extensiv und mit großer Begeisterung. Ein weiteres großes Java-Open-Source-Projekt, das JPMS konsequent nutzt, ist Helidon [3]. Insgesamt wurde JPMS aber bisher vom Java Ecosystem nur sehr zögerlich angenommen.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Dieser Artikel versteht sich als eine logische Fortsetzung von [4] und will Mut machen, sich mit dem JPMS auseinanderzusetzen. Er geht auf Details und Best Practices ein, die über die typischen Hello-World-Tutorials zu Java-Modulen im Web hinausgehen.

Java-Module

Begriffe wie Modul oder auch Komponente werden in der IT nicht präzise definiert und daher in verschiedenen Kontexten unterschiedlich verwendet, was zu großer Verwirrung führen kann. Relativ gut kann man sich darauf einigen, dass es sich dabei um geschlossene Funktionseinheiten mit definiertem API handelt und dadurch eine bessere Kapselung erreicht wird. In Java ist ein Modul eine Einheit, wie man es von einer JAR-Datei oder einem Maven-Modul kennt, die über einen spezifischen Moduldeskriptor verfügt (oder für die als Automatic Module ein Moduldeskriptor dynamisch erzeugt wird).

Der Moduldeskriptor

Dieser Deskriptor wird im Source Code in der Datei module-info.java im Default-Package definiert. Es handelt sich hierbei um ein völlig anderes Konstrukt als Class, Interface, Enum, Annotation oder Record. Beim Build wird aus dem Modul eine JAR-Datei, die im Wurzelverzeichnis eine module-info.class-Datei enthält. Der Modul-Deskriptor definiert eine ganze Reihe wichtiger Eigenschaften des Moduls:

  • Name des Moduls
  • Abhängigkeiten von anderen Modulen
  • Liste der exportieren Packages
  • Angebotene sowie verwendete Services
  • Reflection-Zugriffe

Modulname und Packages

Der Name des Moduls dient als globaler, weltweit eindeutiger Identifier, um das Modul zu referenzieren. Er folgt den gleichen Syntaxregeln wie ein Package, ist davon aber prinzipiell völlig unabhängig. Nutzer von Maven oder Gradle können es sich als Kombination von Group-ID und Artifact-ID vorstellen. Es hat sich als Best Practice herausgestellt, Package- und Modulnamen sowie Artifact-ID in Einklang zu bringen. Das bedeutet, dass sich der Modulname als Namensraum bzw. Präfix in den Packages, die im Modul enthalten sind, wiederfindet. Diese Konvention hat auch den Vorteil, dass Entwickler, die ein Modul verwenden möchten, einfach zwischen Modulnamen und Packages hin und her mappen können, ohne in einer externen Dokumentation nachschauen zu müssen. Eine Klasse aus einem bekannten Package zu verwenden, heißt zwar noch nicht, dass der Modulname mit dem Package-Namen übereinstimmt, aber wenn der Modulname ein Präfix davon ist, kann er in modernen IDEs mit Autovervollständigung gezielt gefunden werden.

Zudem sind alle Java-Entwickler bereits mit den Best Practices für Packages samt den Namensräumen als „umgedrehten Domainnamen“ vertraut. Zur Veranschaulichung stellen wir uns als konkretes Beispiel vor, dass wir für awesome-restaurant.com arbeiten und gerade das Backend für Onlinereservierungen und -bestellungen bauen. Der Java-Code verwendet für Bestellungen bereits Packages wie com.awesome_restaurant.online.order.domain oder com.awesome_restaurant.online.order.repository. Da ein Minuszeichen in Package-Namen nicht zulässig ist, wurde es beim Mapping der Domain auf Java-Packages durch einen Unterstrich ersetzt (man kann es natürlich auch weglassen oder durch einen Punkt ersetzen).

Möchte ich nun das Backend als Java-Modul bereitstellen, könnte meine module-info.java-Datei so aussehen:

module com.awesome_restaurant.online.order {
  requires jakarta.persistence;
  // ...
  exports com.awesome_restaurant.online.order.domain;
  // ...
}

Abhängigkeiten von anderen Modulen

Abhängigkeiten werden in der module-info.java-Datei durch das Schlüsselwort requires eingeleitet. Am Ende folgt der Name des angeforderten Moduls. Im obigen Beispiel ist das jakarta.persistence, der neue Modulname der JPA aus Jakarta EE.

Optional kann requires noch einer der beiden folgenden Modifier folgen:

  • transitiv: Damit wird die Abhängigkeit transitiv, d. h. Module, die mein Modul anfordern, erhalten diese Abhängigkeit automatisch ebenfalls. Andernfalls ist das nicht so und der Code der Abhängigkeit ist für sie zunächst unsichtbar. Anders als bei Maven oder Gradle betrifft diese Eigenschaft nur die Sichtbarkeit und ändert nichts daran, dass die Abhängigkeit zur Laufzeit gebraucht wird. Somit wird beim JPMS sehr genau zwischen transitiven und nichttransitiven Abhängigkeiten unterschieden: Ich deklariere Abhängigkeiten von einem anderen Modul nur dann als transitiv, wenn es für die Verwendung meines Moduls (via API) auch für den Aufrufer sichtbar sein muss oder etwas völlig Querschnittliches ist, wie z. B. das SLF4J API zum Logging. Tauchen z. B. Typen aus diesem Modul in meinem eigenen API auf, dann deklariere ich die Abhängigkeit als transitiv. Andernfalls erhalte ich von meiner IDE eine Warnung. Abhängigkeiten, die ich nur intern für meine Implementierungen benötige, deklariere ich nicht transitiv und vermeide damit das Namespace-Pollution-Problem.
  • static: Damit wird eine Abhängigkeit optional, d. h., das angeforderte Modul ist zur Laufzeit nicht erforderlich und das JPMS wirft keinen Fehler, wenn es fehlt. Natürlich muss ich meinen Code so schreiben, dass er damit umgehen kann. Am Schlüsselwort static sieht man mal wieder, dass der Java-Compiler auf reservierte Schlüsselwörter fokussiert ist. Hier wurde also offensichtlich ein bereits in Java reserviertes Schlüsselwort missbraucht, was für diesen Kontext nicht gerade selbsterklärend ist. Es wäre wesentlich intuitiver gewesen, hier einfach optional statt static zu schreiben.

Damit kommen wir zur ersten starken Einschränkung bei der Verwendung des JPMS: In meinem Modul sehe ich fremden Code nur dann, wenn er aus einem Modul kommt, das als Abhängigkeit per requires importiert wird (inklusive transitiver Abhängigkeiten). Das betrifft sogar Klassen aus dem JDK – bis auf solche, die aus dem Modul java.base stammen. Das ist super, weil es das eingangs erwähnte Problem der Namespace Pollution löst.

 

Gleichzeitig bedeutet es aber auch, dass ich nur Code importieren und auf ihn zugreifen kann, wenn er selbst wiederum als Java-Modul bereitsteht. Und damit beginnt schon ein großes Dilemma: Die coole Open-Source-Bibliothek, die ich gerne nutzen möchte, ist kein Modul? Dann kann ich sie in meinem Modul erst mal nicht verwenden.

An dieser Stelle möchten wir die verschiedenen Arten von Java-Modulen differenzieren:

  • Systemmodule werden vom JRE bzw. JDK bereitgestellt. Mit dem Projekt Jigsaw (auf Deutsch Stichsäge) hat Oracle die Mammutaufgabe gemeistert, das JDK als historisch gewachsenen Codehaufen sauber in Java-Module mit definierten Abhängigkeiten zu zerschneiden. Das beschleunigt den Start der JVM. Module wie java.desktop (Swing) oder java.rmi mit all ihren Klassen müssen nicht geladen werden, wenn man sie nicht braucht.
  • Custom- oder Application-Module sind Module des Java Ecosystem, die explizit über einen Moduldeskriptor (module-info.class) verfügen, aber nicht zum JDK gehören. Auf ihnen liegt der Fokus dieses Artikels.
  • Automatic Module sind ein Behelfsmittel für das angesprochene Problem, eine Bibliothek aus einem Modul zu nutzen, die selbst keinen Moduldeskriptor hat. Entwickler von (Open-Source-)Bibliotheken für Java, die keine Lust haben, Moduldeskriptoren für ihre JARs zu pflegen, sollten unbedingt in ihrer Manifest-Datei einen Modulnamen über die Property Automatic-Module-Name festlegen. Damit kann das JAR über diesen Modulnamen importiert werden und alle Packages sind automatisch exportiert. Ist auch das nicht der Fall, bleibt als letzter Notanker nur die Möglichkeit, die Bibliothek nach bestimmten Regeln über den Kernbestandteil des Dateinamens der JAR-Datei anzusprechen. Das mag zum Ausprobieren einen Versuch wert sein – ich rate jedoch davon ab, es als finale Lösung für ein Release zu verwenden. Stattdessen sollte man die Autoren der Bibliothek per Feature-Request-Ticket überzeugen, einen Automatic-Module-Name festzulegen oder man verzichtet einfach auf den Einsatz der Bibliothek und sucht eine Alternative.
  • Unnamed Module: In Java gibt es genau ein Unnamed Module, in dem automatisch alles landet, was kein Modul ist. Andere Module können aber nicht auf Code daraus zugreifen.

Neue Spielregeln für Packages und Reflection

Die Spielregeln beim JPMS sind vom Prinzip her das Gegenteil von dem, was man als Java-Entwickler mit den normalen Classpaths gewohnt ist: Im normalen Java ist grundsätzlich alles sichtbar und erlaubt, außer, wir schränken es mit Schlüsselwörtern, wie z. B. private ein. Packages selbst sind immer public. Beim JPMS ist mein eigener Code zunächst nur innerhalb meines Moduls sichtbar. Will ich das ändern, muss ich Packages explizit exportieren. Hierzu verwenden wir in der module-info.java-Datei das Schlüsselwort exports, gefolgt von dem Package, das wir veröffentlichen möchten. Das geht nur für Packages, die im Modul auch existieren und nicht leer sind. Ich muss also jedes einzelne Package, das ich exportieren möchte, explizit auflisten.

Eine Syntax, die alle Packages automatisch exportiert, ist nicht vorgesehen. Das ist auch sinnvoll, denn so wird verhindert, dass ein nachträglich hinzugefügtes Package aus Versehen exportiert und damit öffentlich wird. Es geht hierbei um das Geheimnisprinzip, das der erfahrene Java-Entwickler aus der Softwarearchitektur kennt. Nur beim Automatic Module werden automatisch alle Packages exportiert und damit das gesamte Verhalten von JARs im Classpath für Module simuliert.

Die Restriktionen gehen aber noch weiter: Ich kann keinen Code in ein Package platzieren, in das bereits ein anderes Modul Code platziert – auch wenn ich als Autor dieses anderen Moduls alles unter meiner Kontrolle habe und sogar dann nicht, wenn das Package nicht exportiert ist. Dieses sogenannte Split-Package-Konstrukt wird in konventionellem Java manchmal eingesetzt, um Sichtbarkeiten bewusst zu hintergehen: Ich lege meine eigene Klasse ins Package org.hibernate, um an eine Methode von Hibernate zu kommen, die protected oder default als Sichtbarkeit hat. Und wenn dieser Hack nicht reicht, hole ich den Holzhammer raus und nutze Reflection und mache mit der Methode setAccessible(true) alles zugänglich. Um die Katze vollständig aus dem Sack zu lassen: Reflection geht in JPMS per Default auch nicht mehr, außer, sie wird explizit erlaubt.

Als ich mich zum ersten Mal mit dem JPMS auseinandergesetzt habe, war spätestens das der Moment, wo ich geschluckt und das Thema erstmal beiseite gewischt habe. Das ist vermutlich auch der Grund, warum viele Artikel im Web so negativ über das JPMS berichten und vom Einsatz abraten. Das ist aber sehr schade, denn meines Erachtens gründet diese Reaktion ausschließlich in der Macht der Gewohnheit.

IT ist ständig im Wandel und Patterns, die vor zehn oder zwanzig Jahren unser Denken bestimmt haben, sind heute überholt und wurden durch neue ersetzt. Aus Sicht von IT-Security und Zero Trust ist das Design des JPMS genial für einen Neustart. Sind nicht gerade Reflection und Serialisierung die Wurzel aller Übel der gravierenden CVEs in der Java-Welt? Und ist nicht die Verwendung von (Deep) Reflection zur Laufzeit spätestens seit dem Erscheinen von GraalVM, Quarkus und Co. ein Konstrukt des vergangenen Jahrtausends? Mit JEP 411 wurde der Security Manager beerdigt und man sollte sich die Frage stellen, ob man nicht auf einem sinkenden Schiff segelt, wenn man sich nicht mit dem JPMS auseinandersetzt.

Wer sich in diesem Sinne auf das JPMS einlassen kann, der wird sich nach einer gewissen Eingewöhnungszeit fragen, warum Java nicht von Anfang an so konzipiert wurde. Mit dem konkreten Ausprobieren kommt auch die Erfahrung, um die Vor- und Nachteile abwägen zu können und zu wissen, in welchem Vorhaben der Einsatz sinnvoll ist und wo eher nicht. Der Vollständigkeit halber sei noch erwähnt, dass das Schlüsselwort open vor module im Moduldeskriptor das ganze Modul per Reflection freigibt oder das Schlüsselwort opens dies nur für ein explizites Package erlaubt (analog zu exports). Im Allgemeinen rate ich von diesem Konstrukt jedoch ab.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

In meiner Zeit vor dem JPMS verwendete ich in meinen Packages Segmente wie api oder impl, um das auszudrücken, was ich ohne JPMS nicht ausdrücken konnte. Mit externen Tools wie SonarQube konnte ich dann ungewollte Zugriffe aus einem api-Package auf ein impl-Package aufdecken. Inzwischen kann ich auf künstliche Segmente wie api verzichten und über meinen Moduldeskriptor festlegen, welches Package zum API gehört und daher exportiert wird. Die Implementierungen können immer noch in impl-Sub-Packages liegen, wenn man es explizit machen möchte. Da diese für andere Module unsichtbar sind, können die Packages und sämtliche enthaltenen Klassen nach Belieben umbenannt werden, ohne Gefahr zu laufen, dass ein Nutzer des Moduls beim nächsten Release Compilerfehler erhält. Das Beste ist, dass alle diese internen Klassen und ihre Methoden nach Belieben public sein dürfen und der Spagat zwischen flexibler interner Nutzung und Verhinderung ungewollter externer Nutzung beendet ist.

Was aber, wenn mein Projekt aus vielen Modulen besteht und ich bestimmte Zugriffe für eigene Module erlauben und für fremde Module verbieten will? Kein Problem! Im Moduldeskriptor kann ich dazu beim exports-Statement nach dem Package und dem Schlüsselwort to einen oder mehrere Modulnamen angeben, auf die der Zugriff eingeschränkt wird. In unserem Beispiel könnte das so aussehen:

exports com.awesome_restaurant.online.order.repository to
com.awesome_restaurant.online.order.app,
com.awesome_restaurant.online.reservation.app;

Wichtig zum Verständnis ist, dass nach exports ein Package kommt und nach to Modulnamen. Wildcards, um gleich einen ganzen Namensraum zu erlauben (z. B. com.awesome_restaurant.*), werden dabei leider nicht unterstützt.

Services 2.0

In Java gibt es seit der Version 1.6 den ServiceLoader, um Services dynamisch anzufordern, die sich über textuelle Dateien unter META-INF/services konfigurieren lassen. Das Konstrukt hat keine übermäßige Verbreitung gefunden und ist vielen Java-Entwicklern unbekannt. In Frameworks wie Spring oder Quarkus hat man mit Dependency Injection bereits mächtigere Werkzeuge für dieses Problem.

Mit dem JPMS erfährt der ServiceLoader aber ein Facelifting und damit eine Wiedergeburt: Im Moduldeskriptor können angebotene und genutzte Services Refactoring-stabil angegeben werden. Damit können Module Implementierungen einer Schnittstelle (oder abstrakten Klasse) anfordern, und beliebige andere Module können dazu Implementierungen bereitstellen. Dieses Konstrukt ermöglicht es, mit purem Java, unabhängig von irgendwelchen Frameworks (und Reflection Magic), dynamische Services umzusetzen. Egal ob mit ServiceLoader oder Frameworks wie Spring haben sich für mich zwei Muster herauskristallisiert:

  • Singleton: Ich möchte zu einem Interface genau eine Instanz haben. Woher die Implementierung kommt und wie diese zusammengebaut wird, ist für die Nutzung abstrahiert und wird vom Framework oder eben von Java geregelt. Ein Beispiel wäre ein Interface ConfigurationService oder der berüchtigte EntityManager.
  • Plug-ins: Ich möchte etwas flexibel erweiterbar gestalten und biete selbst ein Interface an, zu dem es auch zur Laufzeit viele unterschiedliche Implementierungen parallel geben darf, die wir Plug-ins nennen. Sie erweitern den Funktionsumfang und dazu können unterschiedliche Parteien in Harmonie beitragen. Als Beispiel stellen wir uns vor, dass wir eine Suchmaschine programmieren, und bieten als Plug-in-Interface TextExtractor an, das zu einem bestimmten Dateiformat den reinen Text extrahieren kann. Ein Modul stellt eine Implementierung PdfTextExtractor bereit, ein anderes XpsTextExtractor usw. Keins dieser Module exportiert irgendetwas, und die nutzende Anwendung importiert diese Module, ohne direkten Zugriff auf deren Klassen zu erhalten.

 

In beiden Fällen füge ich im Modul, das auf den Service zugreifen möchte, ein uses-Statement in den Modul-Deskriptor ein und gebe dabei das Interface des Service an:

module org.search.engine {
  uses org.search.engine.extractor.TextExtractor;
  exports org.search.engine;
  exports org.search.engine.extractor;
  // ...
}

Um im Code an Implementierungen des Service TextExtractor zu gelangen, erzeuge ich einen passenden ServiceLoader und iteriere über die verfügbaren Implementierungen:

ServiceLoader<TextExtractor> serviceLoader = ServiceLoader.load(TextExtractor.class);
for (TextExtractor extractor : serviceLoader) {
  register(extractor);
}

Dabei ist es wichtig, dass der Aufruf von ServiceLoader.load im Code des Moduls stattfindet, in dem das uses-Statement definiert ist. Wer eine generische Hilfsklasse in ein anderes Modul auslagert, die den ServiceLoader anhand eines übergebenen Class-Objekts lädt, wird das schmerzlich bemerken. Die Hilfsklasse habe ich in meinem Projekt trotzdem gebaut, muss aber den ServiceLoader außerhalb im verantwortlichen Modul erzeugen und ihn dann an die Hilfsklasse übergeben. Nutzen entsteht aber erst dann, wenn für den Service auch mindestens eine Implementierung bereitgestellt wird.

In einem anderen Modul implementieren wir dazu besagten PdfTextExtractor und stellen diese Implementierung über ein provides-Statement im Moduldeskriptor zur Verfügung:

module com.company.search.extractor.pdf {
  provides org.search.engine.extractor.TextExtractor
  with com.company.search.extractor.pdf.PdfTextExtractor;
}

Eine Anwendung, die das Ganze im Zusammenspiel verwenden möchte, importiert die Suchmaschine samt der gewünschten Plug-in-Module:

module com.awesome_restaurant.online.app {
  requires org.search.engine;
  requires com.company.search.extractor.pdf;
  requires com.enterprise.search.extractor.xps;
  // ...
}

Brauche ich den XPS-Support nicht mehr, entferne ich einfach das entsprechende requires-Statement. Will ich ein anderes Format unterstützen, lade ich das passende Plug-in über eine weitere requires-Anweisung (wobei das requires-Statement optional ist, solange sich das Modul mit dem Plug-in im Modulpfad befindet).

Hier noch einige Tipps aus dem praktischen Umgang damit:

  • Findet der ServiceLoader zur Laufzeit für den angeforderten Service gar keine oder „zu viele“ Implementierung(en), so muss ich mich selbst um die Fehlerbehandlung kümmern.
  • Für einen Singleton Service möchte ich eine Exception, wenn keine Implementierung gefunden wurde. Gegebenenfalls möchte ich aber auch eine Default- bzw. Fallback-Implementierung mitliefern, die dann verwendet wird.
  • Finde ich für einen Singleton Service mehrere Implementierungen, will ich typischerweise auch eine Exception. Doch der ServiceLoader und das Java-Modul bieten nicht viel Flexibilität (wie z. B. DI-Frameworks aus Spring oder Quarkus): Eine Möglichkeit, einen Service durch ein Modul wieder zu entfernen oder zu ersetzen, gibt es nicht. Mit extrem feingranularen Modulschnitten kann man das alles irgendwie lösen, aber das überfordert schnell die Nutzer meiner Module. Daher habe ich mir angewöhnt, Implementierungen aus meinem eigenen Package Namespace bei Singleton Services zu ignorieren, falls es auch eine Implementierung aus einem externen Package gibt. Damit erlaube ich externen Nutzern meiner Bibliotheken, meine Implementierung durch ihre eigene zu ersetzen, und erhöhe die Flexibilität.

Internationalisierung

Java bietet mit ResourceBundle und MessageFormat die Grundlagen für die Internationalisierung einer Anwendung, also für die Unterstützung mehrerer Sprachen z. B. bei Texten für den Endnutzer. Das Laden eines solchen ResourceBundle aus properties-Dateien (z. B. UiMessages_de.properties) lädt jedoch eine Ressource per ClassLoader, was wie ein Reflection-Zugriff gewertet wird. Daher gibt es Probleme, wenn man diese properties-Dateien in einem Modul verpackt und dann generischen Code aus einem anderen Modul diese als ResourceBundle laden will. Solche Aspekte sind in Java nicht klar dokumentiert und es hat mich initial etwas Zeit gekostet, das vollständig zu verstehen. Um nicht das komplette Modul mit dem Schlüsselwort open für Reflection freizugeben, ist die pragmatischste Lösung, Bundles zur Internationalisierung in separaten JAR-Dateien ohne Moduldeskriptor auszuliefern. Sind sie im Class-/Modulpfad, so landen sie im Unnamed Module und können ohne Probleme geladen werden.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

IDE-Unterstützung

Als ich die ersten Versuche mit Java-Modulen gemacht habe, war die Unterstützung in gängigen Entwicklungsumgebungen katastrophal bis nicht vorhanden. Inzwischen hat sich das deutlich stabilisiert und auch Code Completion und Refactoring werden unterstützt. Ich kann sowohl in IntelliJ als auch in Eclipse komplexe Projekte mit vielen Java-Modulen zuverlässig entwickeln. In Eclipse gibt es noch diverse Bugs bei module-info.java-Dateien mit dem Code-Formatter und insbesondere mit Import-Statements. Ich verzichte daher einfach auf Imports in diesen Dateien und verwende voll qualifizierte Typen. Auch bei IntelliJ gibt es Kinderkrankheiten, wie z. B. IllegalAccessError, weil irgendein Teil von JUnit im Unnamed Module gelandet ist.

Tests

Im einfachen Fall schreibt man seine JUnit-Tests ganz normal wie in Projekten ohne Java-Module. Will man aber im Test ein zusätzliches Testmodul haben, einen Service zum Testen hinzufügen oder Ähnliches, gibt es Probleme. Eigentlich würde man gern unter src/test/java eine zusätzliche module-info.java hinterlegen können, die dann für den Test verwendet wird. Genau das wird auch in der Maven-Dokumentation als Lösung propagiert – wird aber von IDEs gar nicht unterstützt [5]. Als Workaround erstelle ich dann zusätzliche Module, die nur zum Testen dienen, und schließe diese vom Deployment aus, sodass sie nicht im Release veröffentlicht werden.

Entscheidung für oder gegen JPMS

Im nächsten Spring-Boot- oder Quarkus-Projekt würde ich persönlich JPMS trotzdem noch nicht nutzen. Diese Frameworks basieren maßgeblich auf Reflection, und mit Java-Modulen schaffe ich mir hier viele Probleme, ohne großen Nutzen zu stiften. Inzwischen ist es immerhin technisch möglich, was schon mal als großer Fortschritt zu werten ist. Wenn ich ein Backend mit JPMS ausprobieren will, kann ich das mit Helidon tun, das besser für JPMS geeignet ist, jedoch im Vergleich zu Spring Boot und Quarkus eher ein Schattendasein fristet. Micronaut kann mit dem JPMS aktuell gar nicht verwendet werden [6].

Schreibe ich hingegen eine Java-App ohne große Frameworks, sollte ich das JPMS ausprobieren. Entwickle ich eine Bibliothek, die möglichst viele Nutzer erreichen soll, ist die Unterstützung des JPMS Pflicht. Andernfalls schließe ich diverse potenzielle Nutzer von vornherein aus.

Fazit und Ausblick

Im Jahr 2018 konnte man das JPMS zwar schon vollständig nutzen, kämpfte aber gegen Windmühlen. In der heutigen Zeit hat sich das JPMS so weit etabliert, dass die Unterstützung sämtlicher Tools ausgereift und die der wichtigen Java-Bibliotheken gegeben ist. Es ist also an der Zeit, sich intensiver mit dem Thema auseinanderzusetzen und eigene Experimente und Erfahrungen zu machen. Für Entwickler von Bibliotheken ist es heutzutage Pflicht, Moduldeskriptoren (mindestens als Automatic-Module-Name) mitzuliefern.

Wenn sich Java weiter weg von Deep Reflection zur Laufzeit hin zu AoT und CWA entwickelt und auch die Frameworks sich entsprechend weiterentwickeln, könnte die Verwendung des JPMS in der Zukunft zumindest für Green-Field-Projekte zum Standard werden.


Links & Literatur

[1] https://openjdk.org/jeps/411

[2] https://m-m-m.github.io

[3] https://github.com/helidon-io/helidon

[4] Langer, Angelika; Kreft, Klaus: „Eine Anwendung in vielen Teilen. Project Jigsaw aka Java Module System“; in: Java Magazin 10.2017, https://entwickler.de/reader/reading/java-magazin/10.2017/1976daae3f43cab4336cd7ad

[5] https://github.com/eclipse-jdt/eclipse.jdt.core/issues/1465

[6] https://github.com/micronaut-projects/micronaut-core/issues/6395

Top Articles About Core Java & Languages

Alle News der Java-Welt:

Behind the Tracks

Agile, People & Culture
Teamwork & Methoden

Clouds & Kubernetes
Alles rund um Cloud

Core Java & Languages
Ausblicke & Best Practices

Data & Machine Learning
Speicherung, Processing & mehr

DevOps & CI/CD
Deployment, Docker & mehr

Microservices
Strukturen & Frameworks

Performance & Security
Sichere Webanwendungen

Serverside Java
Spring, JDK & mehr

Software-Architektur
Best Practices

Web & JavaScript
JS & Webtechnologien

Digital Transformation & Innovation
Technologien & Vorgehensweisen

Domain-driven Design
Grundlagen und Ausblick

Spring Ecosystem
Wissen in Spring-Technologien

Web-APIs
API-Technologie, Design und Management

ALLE NEWS ZUR JAX!